import sys
import threading
import socket
import zlib
import wx
#import wx.lib.newevent

##sys.path.append('../itrc-automation/recorder')
##import socketio

#idRecvEvt = wx.NewEventType()
#wx.PyEvent(eventType=idRecvEvt)
class SocketEvent(wx.PyEvent):
    eventType = wx.NewEventType()
    def __init__(self, sock, event, errno):
#        wx.PyEvent.__init__(self, winid=src.GetId(), eventType=self.eventType)
        wx.PyEvent.__init__(self, eventType=self.eventType)
 #       self.EventObject = src
        self.sock = sock
        self.event = event
        self.errno = errno

EVT_SOCKET = wx.PyEventBinder(SocketEvent.eventType)

FD_READ    = 1 << 0
FD_WRITE   = 1 << 1
FD_OOB     = 1 << 2
FD_ACCEPT  = 1 << 3
FD_CONNECT = 1 << 4
FD_CLOSE   = 1 << 5

from utils import delegate
from select import select
from errno import EWOULDBLOCK
from _socket import *
SocketError = error

@delegate(SocketType, 'sock', ('bind', 'shutdown', 'fileno',
                               'getsockopt', 'setsockopt',
                               'getsockname', 'getpeername'))
class AsyncSocket(object):

    def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0):
        if isinstance(family, SocketType):
            self.sock = family
            self.state = 'e'
        else:
            self.sock = socket(family, type, proto)
            self.state = None
        self.thrd = None
        self.callback = None
        self.events = 0

    def connect(self, addr):
        self.state = 'c'
        try:
            return self.sock.connect(addr)
        except SocketError as err:
            if err.errno == EWOULDBLOCK:
                return None
            raise

    def listen(self, backlog):
        self.state = 'l'
        ret = self.sock.listen(backlog)
        if self.events & FD_ACCEPT:
            self.rfds = (self,)
            self.csw.send('.')

    def accept(self):
        sock, addr = self.sock.accept()
        if self.events & FD_ACCEPT:
            self.rfds = (self,)
            self.csw.send('.')
        sock = AsyncSocket(sock)
        if self.events:
            sock.async_select(self.callback, self.events)
        return sock, addr

    def recv(self, bufsize, flags=0):
        ret = self.sock.recv(bufsize, flags)
        if self.events & FD_READ and not flags & MSG_OOB and ret:
            self.rfds = (self,)
            self.csw.send('.')
        elif self.events & FD_OOB and flags & MSG_OOB:
            self.xfds = (self,)
            self.csw.send('.')
        return ret

    def send(self, data, flags=0):
        try:
            return self.sock.send(data, flags)
        except SocketError as err:
            if self.events & FD_WRITE and err.errno == EWOULDBLOCK:
                self.wfds = (self,)
                self.csw.send('.')
            raise

    def close(self):
        if self.thrd:
            self.csw.send('q')
            self.thrd.join()
            self.csw.close()
            self.csr.close()
            self.thrd = None
        return self.sock.close()

    def async_select(self, callback, events):
        self.callback = callback
        self.events = events
        self.rfds = (self,) if events & (FD_READ | FD_ACCEPT | FD_CLOSE) else ()
        self.wfds = (self,) if events & (FD_WRITE | FD_CONNECT) else ()
        self.xfds = (self,) if events & (FD_OOB | FD_CONNECT) else ()
        if self.thrd is None:
            self.csr = socket(AF_INET, SOCK_DGRAM)
            self.csr.bind(('127.0.0.1', 0))
            self.csw = socket(AF_INET, SOCK_DGRAM)
            self.csw.connect(self.csr.getsockname())
            self.thrd = threading.Thread(target=self.async_proc)
            self.thrd.start()
            self.sock.setblocking(False)
        else:
            self.csw.send('.')

    def async_proc(self):
        print "starting asyncproc"
        while True:
            rfds, wfds, xfds = self.rfds + (self.csr,), self.wfds, self.xfds
##            print 'before select:', len(rfds), len(wfds), len(xfds)
            rfds, wfds, xfds = select(rfds, wfds, xfds)
##            print 'after select:', len(rfds), len(wfds), len(xfds)
            if self.csr in rfds:
                rfds.remove(self.csr)
                if self.csr.recv(1) == 'q':
                    break
            if self.state == 'l':
                if rfds:
                    self.rfds = ()
                    if self.events & FD_ACCEPT:
                        self.callback(self, FD_ACCEPT, 0)
            elif self.state == 'c':
                if rfds or wfds or xfds:
                    self.state = 'e'
                    self.rfds = (self,) if self.events & (FD_READ | FD_CLOSE) else ()
                    self.wfds = (self,) if self.events & FD_WRITE else ()
                    self.xfds = (self,) if self.events & FD_OOB else ()
                    if self.events & FD_CONNECT:
                        errno = self.sock.getsockopt(SOL_SOCKET, SO_ERROR)
                        self.callback(self, FD_CONNECT, errno)
            else:
                if wfds:
                    self.wfds = ()
                    if self.events & FD_WRITE:
                        self.callback(self, FD_WRITE, 0)
                if xfds:
                    self.xfds = ()
                    if self.events & FD_OOB:
                        self.callback(self, FD_OOB, 0)
                if rfds:
                    self.rfds = ()
                    avail = True
                    if self.events & FD_CLOSE:
                        try:
                            avail = self.sock.recv(1, MSG_PEEK)
                            errno = 0
                        except SocketError as err:
                            avail = False
                            errno = err.errno
                        if not avail:
##                            self.state = None
                            self.callback(self, FD_CLOSE, errno)
                    if self.events & FD_READ:
                        if avail:
                            self.callback(self, FD_READ, 0)
##                    elif self.events & FD_CLOSE and not avail:
##                        if not avail:
##                    if self.events & FD_READ:
##                        if avail:
        print "quitting asyncproc"

##    def accept(self):
##        try:
##            return SocketType.accept(self, addr)
##        except SocketError as err:
##            if err.errno == EWOULDBLOCK:
##                return False
##            raise
##
##    def listen(self, addr):
##        assert not self.running
##        self.sock = socket.socket()
##        sock.setsockopt(socket.SOL_SOCKET, socket.SO_OOBINLINE, True)
##        self.thrd = threading.Thread(target=self.listenproc, args=(addr,))
##        self.running = True
##        self.thrd.start()
##
##    def send(self, data):
##        assert self.running
##        self.sock.sendall(data)
##
##    def shutdown(self):
##        assert self.running
##        self.sock.shutdown(socket.SHUT_WR)
##    
##    def connectproc(self, addr):
##        print 'starting connectproc to %s:%s' % addr, self.id
##        self.sock.connect(addr)
##        self.notifyfn(self, 'connect', addr)
##        self.recvproc('>>>')
##        print 'ending connectproc', self.id
##
##    def listenproc(self, addr):
##        print 'starting listenproc to %s:%s' % addr, self.id
##        self.sock.bind(addr)
##        self.sock.listen(1)
##        sock, addr = self.sock.accept()
##        self.sock.close()
##        self.sock = sock
##        self.notifyfn(self, 'accept', addr)
##        self.recvproc('<<<')
##        print 'ending listenproc', self.id
##    
##    def recvproc(self, dir):
##            
##                self.notifyfn(self, 'recv')
##            data = self.sock.recv(4096)
##            print "%s recv %d bytes" % (dir, len(data))
##            if data:
##                self.notifyfn(self, 'recv', data)
##            else:
##                self.notifyfn(self, 'close', None)
##                break
##
##    def close(self):
##        print 'closing AsyncSocket', self.id, '...',
##        if self.running:
##            self.running = False
##            self.sock.close()
##            self.thrd.join()
##        print 'done'


class MyFrame(wx.Frame):

    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, -1, title)
        panel = wx.Panel(self)
        self.output = RawDumpWin(panel)
        self.input = wx.TextCtrl(panel, style=wx.TE_PROCESS_ENTER)
        self.Bind(wx.EVT_TEXT_ENTER, self.OnTextEnter, self.input)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.output, 1, wx.ALL | wx.EXPAND, 10)
        sizer.Add(self.input, 0, wx.ALL | wx.EXPAND, 10)

        subpanel = wx.Panel(panel)
        self.sb = wx.ScrollBar(subpanel, size=(300,-1), style=wx.SB_HORIZONTAL)
#        self.sb.SetFocus()
        sizer.Add(subpanel, 0, wx.ALL | wx.EXPAND, 5)

        bsizer = wx.BoxSizer(wx.HORIZONTAL)
        self.conbtn = wx.Button(panel, -1, "Connect")
        self.Bind(wx.EVT_BUTTON, self.OnConnect, self.conbtn)
        bsizer.Add(self.conbtn, 0, wx.ALL, 5)
        self.disbtn = wx.Button(panel, -1, "Disconnect")
        self.disbtn.Disable()
        self.Bind(wx.EVT_BUTTON, self.OnDisconnect, self.disbtn)
        bsizer.Add(self.disbtn, 0, wx.ALL, 5)

        self.flopbtn = wx.Button(panel, -1, "Flop")
        self.Bind(wx.EVT_BUTTON, self.OnFlop, self.flopbtn)
        bsizer.Add(self.flopbtn, 0, wx.ALL, 5)

        sizer.Add(bsizer, 0, wx.ALL, 5)
        panel.SetSizer(sizer)
        panel.Layout()
        self.input.SetFocus()
        
        self.inconn = AsyncSocket()
        self.inconn.tag = '|||'
        self.inconn.async_select(self.SocketCallback, FD_ACCEPT)
        self.outconn = AsyncSocket()
        self.outconn.tag = '<<<'
        self.outconn.setsockopt(SOL_SOCKET, SO_OOBINLINE, True)
        self.outconn.async_select(self.SocketCallback, FD_CONNECT|FD_READ|FD_WRITE|FD_CLOSE)
##        self.inconn.other = self.outconn
##        self.inconn.tag = '>>>'
##        self.outconn.other = self.inconn

        self.Bind(EVT_SOCKET, self.OnSocket)
        self.Bind(wx.EVT_CLOSE, self.OnClose)

    def OnFlop(self, evt):
        print 'flop!'
#        self.sb.ToggleWindowStyle(wx.BORDER_SUNKEN)
        self.sb.SetForegroundColour('red')
        self.Refresh()
    
    def SocketCallback(self, sock, event, errno):
        self.AddPendingEvent(SocketEvent(sock, event, errno))

    def OnSocket(self, evt):
        if evt.event == FD_CONNECT:
            assert evt.sock is self.outconn
##            err = self.outconn.getsockopt(SOL_SOCKET, SO_ERROR)
            if evt.errno == 0:
                addr = self.outconn.getpeername()
                self.output.append('Established connection to %s:%s' % addr)
            else:
                self.output.append('Connection failed: [%s]' % evt.errno)
        elif evt.event == FD_ACCEPT:
            assert evt.sock is self.inconn
            sock, addr = self.inconn.accept()
            self.output.append('Accepted connection from %s:%s' % addr)
            self.inconn.close()
            self.inconn = sock
            self.inconn.tag = '>>>'
            self.inconn.setsockopt(SOL_SOCKET, SO_OOBINLINE, True)
            self.inconn.async_select(self.SocketCallback, FD_READ|FD_WRITE|FD_CLOSE)
##            self.inconn.other = self.outconn
##            self.outconn.other = self.inconn
##            self.outconn.tag = '<<<'
            self.output.append('Connecting to %s:%s' % self.addr)
            self.outconn.connect(self.addr)
##            self.outconn.async_select(self.SocketCallback, FD_READ|FD_WRITE|FD_CONNECT|FD_CLOSE)
        elif evt.event == FD_READ:
            data = evt.sock.recv(4096)
            print "%s recv %d bytes" % (evt.sock.tag, len(data))
            self.output.append(data, evt.sock.tag)
            if evt.sock is self.inconn:
                self.outconn.send(data)
            else:
                self.inconn.send(data)
        elif evt.event == FD_WRITE:
            pass
        elif evt.event == FD_CLOSE:
            addr = evt.sock.getpeername()
            if evt.errno == 0:
                self.output.append('Connection closed by %s:%s' % addr)
            else:
                self.output.append('Connection reset by %s:%s [%s]' % (addr + (evt.errno,)))
            if evt.sock is self.inconn:
                self.outconn.shutdown(SHUT_WR)
            else:
                self.inconn.shutdown(SHUT_WR)
        else:
            raise RuntimeError('wha?')

    def OnTextEnter(self, evt):
        self.output.append(evt.String)
        self.input.Clear()

    def OnConnect(self, evt):
        self.addr = ('aardmud.org', 23)
        listenaddr = ('', 5577)
        self.output.append('Listening at %s:%s' % listenaddr)
        self.inconn.bind(listenaddr)
        self.inconn.listen(1)
##        self.inconn.async_select(self.SocketCallback, FD_READ|FD_WRITE|FD_ACCEPT|FD_CLOSE)
        self.conbtn.Disable()
        self.disbtn.Enable()

    def OnDisconnect(self, evt):
        self.disbtn.Disable()
        self.conbtn.Enable()
        print 'disconnect!'

##    def OnInConn(self, evt):
##        if evt.kind == 'accept':
##            self.output.AppendText('Accepted connection from %s:%s\n' % evt.data)
##            self.output.AppendText('Connecting to %s:%s\n' % self.addr)
##            self.outconn.connect(self.addr)
##        elif evt.kind == 'recv':
##            self.outconn.send(evt.data)
##            self.output.append(evt.data, True)
##        elif evt.kind == 'close':
##            self.outconn.shutdown()
##            self.output.append('', True)
##        else:
##            raise RuntimeError('wha?')
##        
##    def OnOutConn(self, evt):
##        if evt.kind == 'connect':
##            self.output.AppendText('Established connection to %s:%s\n' % evt.data)
##        elif evt.kind == 'recv':
##            self.inconn.send(evt.data)
##            self.output.append(evt.data, False)
##        elif evt.kind == 'close':
##            self.inconn.shutdown()
##            self.output.append('', False)
##        else:
##            raise RuntimeError('wha?')
        
    def OnClose(self, evt):
        print 'bye-bye!'
        self.inconn.close()
        self.outconn.close()
        self.Destroy()
        
CTLCHRS = { 0:'NUL',  1:'SOH',  2:'STX',  3:'ETX',  4:'EOT',  5:'ENQ',
            6:'ACK',  7:'BEL',  8:'BS',   9:'HT',  10:'LF',  11:'VT',
           12:'FF',  13:'CR',  14:'SO',  15:'SI',  16:'DLE', 17:'DC1',
           18:'DC2', 19:'DC3', 20:'DC4', 21:'NAK', 22:'SYN', 23:'ETB',
           24:'CAN', 25:'EM',  26:'SUB', 27:'ESC', 28:'FS',  28:'GS',
           30:'RS',  31:'US', 127:'DEL'}

TELCMDS = {255:'IAC', 254:'DONT', 253:'DO',  252:'WONT', 251:'WILL',
           250:'SB',  249:'GA',   248:'EL',  247:'EC',   246:'AYT',
           245:'AO',  244:'IP',   243:'BRK', 242:'DM',   241:'NOP',
           240:'SE',  239:'EOR'}

TELOPTS = {0:'BINARY', 1:'ECHO', 3:'SGA', 4:'NAMS', 5:'STATUS',
           6:'TM', 17:'XASCII', 23:'SNDLOC', 24:'TTYPE', 25:'EOR',
           31:'NAWS', 32:'TSPEED', 33:'LFLOW', 34:'LINEMODE',
           35:'XDISPLOC', 37:'AUTH', 38:'ENCRYPT', 39:'ENVIRON',
           41:'XAUTH', 42:'CHARSET', 85:'MCCP1', 86:'MCCP2',
           90:'MSP', 91:'MXP', 93:'ZMP', 102:'AARD', 200:'ATCP',
           201:'GMCP', 255:'EXOPL'}

BINPARM_TELOPTS = {31, 102}

##NBSP = u'\u00a0'
##ELLIP = u'\u2026'
##SPACE = 32
##CR = 13
##LF = 10

IAC  = chr(255)
DONT = chr(254)
DO   = chr(253)
WONT = chr(252)
WILL = chr(251)
SB   = chr(250)
SE   = chr(240)

class RawDumpWin(wx.TextCtrl):
    
    def __init__(self, parent):
        wx.TextCtrl.__init__(self, parent, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2 | wx.TE_DONTWRAP)
        self.decompr = None
        self.submode = None
        self.lasttag = None
        #
        infofnt = wx.Font(10, wx.SWISS, wx.ITALIC, wx.NORMAL)
        datafnt = wx.Font(9, wx.MODERN, wx.NORMAL, wx.NORMAL)
        ctrlfnt = wx.Font(7, wx.TELETYPE, wx.NORMAL, wx.NORMAL)
        hspfnt = wx.Font(7, wx.SWISS, wx.NORMAL, wx.NORMAL)
        vspfnt = wx.Font(3, wx.SWISS, wx.NORMAL, wx.NORMAL)
        #
        self.InfoStyle = wx.TextAttr('black', 'white', infofnt)
        self.SendStyle = wx.TextAttr('black', 'cadet blue', datafnt)
        self.RecvStyle = wx.TextAttr('black', 'pale green', datafnt)
#        self.DataStyle = wx.TextAttr('black', (250,245,210), datafnt)
        self.DataStyle = wx.TextAttr('black', (238, 232, 170), datafnt)
        self.SubTxtStyle = wx.TextAttr('black', (193, 255, 193), ctrlfnt)
        self.SubNumStyle = wx.TextAttr('dark grey', (127, 230, 200), ctrlfnt)
        self.CtlChrStyle = wx.TextAttr('white', 'purple', ctrlfnt)
        self.TelCmdStyle = wx.TextAttr('white', (208, 32, 144), ctrlfnt)
        self.TelOptStyle = wx.TextAttr('white', 'maroon', ctrlfnt)
        self.SpaceStyle = wx.TextAttr('light grey', 'white', hspfnt)
        self.BreakStyle = wx.TextAttr('light grey', 'white', vspfnt)
#        self.DefaultStyle = self.NormStyle
        #
        self.Font = infofnt
        self.MinSize = (self.CharWidth * 84, self.CharHeight * 40)
        print 'minsize is', self.MinSize
        
    def append(self, data, tag=None):
        def AddHeader(usetag):
            self.DefaultStyle = {'>>>': self.SendStyle,
                                 '<<<': self.RecvStyle,
                                  None: self.InfoStyle}[tag]
            if tag:
                self.AppendText(tag if usetag else '   ')
        def AddSpace():
            self.DefaultStyle = self.SpaceStyle
            self.AppendText('|')
        
        if self.lasttag != tag:
            self.DefaultStyle = self.BreakStyle
            self.AppendText('\n')
            self.lasttag = tag
        AddHeader(True)
        if not tag:
            self.AppendText(data + '\n')
            return

##        if data.isalnum():
##            AddSpace()
##            self.DefaultStyle = self.DataStyle if not self.submode else self.SubModStyle
##            self.AppendText(data)
##            return
        if self.decompr and tag == '<<<':
            data = self.decompr.decompress(data)
            if not data and self.decompr.unused_data:
                print 'stopping decompressor'
                data = self.decompr.unused_data
                self.decompr = None
        
        idx = 0
        end = len(data)
        while idx < end:
            jdx = idx
            if self.submode != 'b':
                while jdx < end and (' ' <= data[jdx] <= '~' or
                                     '\x80' <= data[jdx] <= '\xfe'):
                    jdx += 1
                if jdx > idx:
                    AddSpace()
                    self.DefaultStyle = self.DataStyle if not self.submode else self.SubTxtStyle
                    self.AppendText(data[idx : jdx])
                    if jdx == end:
                        break
            ch = data[jdx]
            val = ord(ch)
            if ch == IAC:
                idx = jdx + 2
                for ch in data[jdx : idx]:
                    val = ord(ch)
                    AddSpace()
                    self.DefaultStyle = self.TelCmdStyle
                    self.AppendText(TELCMDS.get(val, '<%d>' % val))
                if ch == SE:
                    if self.submode == 'z':
                        print 'starting decompressor'
                        self.decompr = zlib.decompressobj()
                        if idx < end:
                            data = self.decompr.decompress(data[idx:])
                            idx = 0
                            end = len(data)
                    self.submode = None
                elif SB <= ch <= DONT and idx < end:
                    val = ord(data[idx])
                    idx += 1
                    AddSpace()
                    self.DefaultStyle = self.TelOptStyle
                    self.AppendText(TELOPTS.get(val, '<%d>' % val))
                    if ch == SB:
                        if val == 86:
                            self.submode = 'z'
                        elif val in BINPARM_TELOPTS:
                            self.submode = 'b'
                        else:
                            self.submode =  't'
            elif self.submode:
                idx = jdx + 1
                AddSpace()
                self.DefaultStyle = self.SubNumStyle
                if self.submode == 'b':
                    self.AppendText('%02Xh' % val)
                else:
                    self.AppendText('%d' % val)
            elif val in CTLCHRS:
                idx = jdx + 1
                AddSpace()
                self.DefaultStyle = self.CtlChrStyle
                self.AppendText(CTLCHRS[val])
                if idx < end and (ch == '\n' and data[idx] != '\r' or
                                  ch == '\r' and data[jdx-1:jdx] == '\n'):
                    self.AppendText('\n')
                    AddHeader(False)
##            elif ch == ' ':
##                self.AppendText(ELLIP)
##                idx = jdx + 1
##                continue
            else:
                raise RuntimeError('huh?')
        self.AppendText('\n')

class MyApp(wx.App):
    def OnInit(self):
        frame = MyFrame(None, "Simple wxPython App")
        self.SetTopWindow(frame)
        frame.Show(True)
        return True

if __name__ == '__main__':
    app = MyApp(redirect=False)
    app.MainLoop()

##        # and a few controls
##        text = wx.StaticText(panel, -1, "Hello World!")
##        text.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD))
##        text.SetSize(text.GetBestSize())
##        btn = wx.Button(panel, -1, "Close")
##        funbtn = wx.Button(panel, -1, "Just for fun...")
##
##        # bind the button events to handlers
##        self.Bind(wx.EVT_BUTTON, self.OnTimeToClose, btn)
##        self.Bind(wx.EVT_BUTTON, self.OnFunButton, funbtn)
##
##        # Use a sizer to layout the controls, stacked vertically and with
##        # a 10 pixel border around each
##        sizer = wx.BoxSizer(wx.VERTICAL)
##        sizer.Add(text, 0, wx.ALL, 10)
##        sizer.Add(btn, 0, wx.ALL, 10)
##        sizer.Add(funbtn, 0, wx.ALL, 10)
##        panel.SetSizer(sizer)
##        panel.Layout()

